/**
 */
package multimedia.model;

import java.awt.Point;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Map.Entry;
import java.util.Vector;

import multimedia.model.colorspace.ColorspaceInterface;
import multimedia.model.colorspace.YIQ;
import multimedia.util.DatabaseInterfacer;
import multimedia.util.ProjectConstants;

import org.apache.commons.math.linear.Array2DRowRealMatrix;
import org.apache.commons.math.linear.ArrayRealVector;
import org.apache.commons.math.linear.RealMatrix;
import org.apache.commons.math.linear.RealVector;
import org.apache.commons.math.stat.descriptive.moment.StandardDeviation;

import com.google.common.collect.HashBiMap;


/**
 * This class is used to apply Haar DWT on an object of {@link ModifiedImage}. It does not apply DWT
 * on the object itself, instead it holds a matrix with the transform leaving the image intact. If you want
 * to apply the changes, you must use the extractPixelMatrix method to get the pixels out. This will return
 * the first component zero-ing out the rest, so it is preferable if you use YIQ to get a grayscale instead
 * of a red image.
 * @author trigueros
 *
 */
public class DWT
{
    private ModifiedImage image;
    private RealMatrix imageMatrix;
    private RealMatrix transformedMatrix;
    private int iterationCount;
    private double stdev;
    
     
    
    public DWT() {}
    
    public DWT( ModifiedImage img )
    {
        image = img;
        imageMatrix = extractPixelMatrix( image );
        transformedMatrix = imageMatrix.copy();
//        transformedMatrix = new Array2DRowRealMatrix(imageMatrix.getRowDimension(), imageMatrix.getColumnDimension());
    }
    
    /**
     * Applies multiple iterations of DWT until we can't apply it any longer.
     */
    public void applyTransform()
    {
        // Apply DWT once, and then start dividing the image into smaller sections
        transformedMatrix = applyWaveletTransform(imageMatrix);
        int currentSubWidth = transformedMatrix.getColumnDimension()/2;
        int currentSubHeight = transformedMatrix.getRowDimension()/2;
        iterationCount++;
        
        // Perform DWT Transform until we can't divide the submatrix anymore
        while( currentSubHeight != 1 || currentSubWidth != 1 )
        {
            iterationCount++;
            // Applying the transform to the next left most quadrant, using -1 because it is 0-based.
            RealMatrix transformedSubMatrix = applyWaveletTransform( transformedMatrix.getSubMatrix(0, currentSubHeight - 1, 0, currentSubWidth - 1) );
            
            // Insert the submatrix in the transformedMatrix
            transformedMatrix.setSubMatrix(transformedSubMatrix.getData(), 0, 0);
            
            // Set new values for currents
            currentSubWidth = transformedSubMatrix.getColumnDimension()/2;
            currentSubHeight = transformedSubMatrix.getRowDimension()/2;
        }
        
        // Calculate standard deviation
        RealVector entireMatrix = new ArrayRealVector();
        for( int i = 0; i < transformedMatrix.getColumnDimension(); i++ )
            entireMatrix = entireMatrix.append(transformedMatrix.getColumnVector(i));
        
        StandardDeviation stdev = new StandardDeviation();
        this.stdev = stdev.evaluate(entireMatrix.getData());
    }

    /**
     * @param filename
     * @param imgId
     * @param m
     */
    public void generateFileOutput( String filename, int imgId,  int m )
    {
        // In case someone wants to be funny
        if( m < 1 )
            return;
        
        // Create a vector to store the coordinate of the significant value, format row, col
        Vector<Point> coordinateVector = new Vector<Point>();
        
        // Add the first coordinate to the vector
        coordinateVector.add( new Point(0,0));
        
        // Iterate through matrix in the EZW Scanning order until the end has been reached or until 
        // we have obtained m significant values
        switch( m )
        {
            // If it happens that the user supplies m to be 0 then finish method.
            case 0:
                return;
                
            // If the user supplies 1, then do nothing here but print out that value at end of switch 
            case 1:
                break;
                
            // Here is the bread and butter of the program, this is where we assign the values into coordinateVector 
            default:
                for( int i = 0; i < iterationCount && (int)coordinateVector.size() < m; i++ )
                {
                    // Special case when i = 0
                    int offset = ( i == 0 ) ? 1 : (int)Math.pow(2, i);
                    RealMatrix subMatrix = new Array2DRowRealMatrix(offset,offset);
                    
                    // Special case when i = 0
                    if( i == 0 )
                    {
                        // Add top right
                        if( (int)coordinateVector.size() < m )
                        {
                            if( transformedMatrix.getEntry(0, offset) > stdev )
                                coordinateVector.add( new Point( 0, offset ));
                        }
                        else
                            break;
                        
                        // Add bottom left
                        if( (int)coordinateVector.size() < m )
                        {
                            if( transformedMatrix.getEntry(offset, 0) > stdev )
                                coordinateVector.add( new Point(offset, 0));
                        }
                        else
                            break;
                        
                        // Add diagonal
                        if( (int)coordinateVector.size() < m )
                        {
                            if( transformedMatrix.getEntry(offset, offset) > stdev )
                                coordinateVector.add( new Point( offset, offset ));
                        }
                        else
                            break;                            
                    }
                    else
                    {
                        Vector<Point> points = new Vector<Point>();
                        
                        // Add top right
                        subMatrix = transformedMatrix.getSubMatrix(0 , offset - 1, offset, offset*2 - 1);
                        obtainValidValues(  points, subMatrix, 0, offset );
                        
                        // Add bottom right
                        subMatrix = transformedMatrix.getSubMatrix(offset, offset*2 - 1, 0, offset - 1 );
                        obtainValidValues(  points, subMatrix, offset, 0 );
                        
                        // Add diagonal values
                        subMatrix = transformedMatrix.getSubMatrix(offset, offset*2 - 1, offset, offset*2 - 1 );
                        obtainValidValues(  points, subMatrix, offset, offset );
                        
                        int pointIndex = 0;
                        while( coordinateVector.size() < m  && pointIndex < points.size() )
                        {
                            coordinateVector.add(points.get(pointIndex++));
                        }
                    }
                }
                break;
        }
        
        Vector<String> outputData = new Vector<String>();
        
        // Dump the output to a file
        for( int i = 0; i < coordinateVector.size(); i++ )
        {
            outputData.add( new String( imgId + "/" + coordinateVector.get(i).x + "," + coordinateVector.get(i).y + "/" + transformedMatrix.getEntry(coordinateVector.get(i).x, coordinateVector.get(i).y) ));
        }
        DatabaseInterfacer.WriteToDatabase(filename, outputData);
    }

    private void obtainValidValues( Vector<Point> points, RealMatrix subMatrix, int x_offset, int y_offset )
    {
        for( int row = 0; row < subMatrix.getRowDimension(); row++ )
            for( int col = 0; col < subMatrix.getColumnDimension(); col++ )
            {
                double entry = subMatrix.getEntry(row, col);
                if( Math.abs(entry) > stdev )
                {
                    points.add(new Point(row + x_offset ,col + y_offset));
                }
            }
    }

    /**
     * Converts the matrix that has been modified into a colorspace vector that can be 
     * used to display the image.
     * @return the Colorspace Vector
     */
    public Vector<ColorspaceInterface> matrixToColorVector() 
    {
        Vector<ColorspaceInterface> newVector = new Vector<ColorspaceInterface>();
        for( int x = 0; x < transformedMatrix.getRowDimension(); x++ )
            for( int y = 0; y < transformedMatrix.getColumnDimension(); y++ )
            {
                newVector.add( new YIQ((float) transformedMatrix.getEntry(x, y), 0.0f,0.0f));
            }
        return newVector;
    }
    
    /**
     * Applies one iteration the Haar Wavelet Transform to a matrix of MxN "things" and 
     * returns the matrix. This can be called on sub parts of a bigger matrix to achieve the 
     * effect of multiple iterations. 
     * @param subMatrix the matrix to which we need to apply the Haar DWT
     * @return the transformed matrix
     */
    private RealMatrix applyWaveletTransform( RealMatrix subMatrix )
    {
        /* First Part of Wavelet Transform */
        
        // Allocated space for transformed matrix
        RealMatrix transformedMatrix = new Array2DRowRealMatrix(subMatrix.getData());
        // Create Haar Matrix based on input matrix
        RealMatrix haarMatrix = calculateHaar1DMatrix(subMatrix.getRowDimension());
        
        // Perform multiplication of Haar Matrix
        transformedMatrix = haarMatrix.multiply(subMatrix);
        
        /* Second Part of Wavelet Transform */

        // Calculate the transpose
        haarMatrix = calculateHaar1DMatrix(subMatrix.getColumnDimension()).transpose();
        
        // Perform multiplication of Haar Matrix again
        transformedMatrix = transformedMatrix.multiply(haarMatrix);       
        
        return transformedMatrix;
    }
    
    /**
     * Copies the pixel's first component into a matrix object.
     * @param image
     * @return
     */
    private RealMatrix extractPixelMatrix( ModifiedImage image )
    {
        // Allocate space for the matrix
        RealMatrix pixelMatrix = new Array2DRowRealMatrix(image.getImageSize().height, image.getImageSize().width);
        
        // Copy the image into the matrix
        for( int row = 0; row < pixelMatrix.getRowDimension(); row++ )
            for( int col = 0; col < pixelMatrix.getColumnDimension(); col++ )
            {
                // Adding the first component of the pixel to the matrix
                pixelMatrix.setEntry(row, col, image.getImagePixelHolder().getPixelAt(col, row).getFirstComponent() );
            }
        
        return pixelMatrix;
    }
    
    /**
     * Generates a Haar Matrix depending on the dimension required.
     * @param dim dimension of the Haar square matrix
     * @return a dim x dim RealMatrix
     */
    private RealMatrix calculateHaar1DMatrix(int dim)
    {
        RealMatrix haarMatrix = new Array2DRowRealMatrix(dim, dim);
        
        // Create the top part of the matrix, the one that calculates the average
        for( int col = 0, row = 0; row < dim/2; col+=2 , row++)
        {
            haarMatrix.setEntry(row,col, 0.5);
            haarMatrix.setEntry(row,col+1, 0.5);
        }
        
        // Create the bottom part of the matrix, the one that calculates the differences
        for( int col = 0, row = dim/2; row < dim; col+=2 , row++)
        {
            haarMatrix.setEntry(row,col, -0.5);  
            haarMatrix.setEntry(row,col+1, 0.5);
        }
        
        return haarMatrix;
    }

    public static Vector<Integer> ObtainMatches( DWT haarInput, String database )
    {
        HashBiMap<Integer, Double> deltaTable = HashBiMap.create();
        Vector<String> databaseInfo = DatabaseInterfacer.ReadDatabase( database );
        
        // Getting the differences
        for( int i = 0; i < databaseInfo.size(); i++ )
        {
            String[] parsedEntry = databaseInfo.get(i).split("/");
            int id = Integer.parseInt(parsedEntry[0]);
            String[] strPoint = parsedEntry[1].split(",");
            int row = Integer.parseInt(strPoint[0]);
            int col = Integer.parseInt(strPoint[1]);
            
            double value = Double.parseDouble(parsedEntry[2]);
            
            if( deltaTable.containsKey(new Integer(id)) )
            {
                double oldValue = deltaTable.get(new Integer(id));
                double actualValue = haarInput.getTransformedMatrix().getEntry(row, col);
                double newEntry = Math.abs(Math.abs(actualValue) - Math.abs(value)) + oldValue;
                deltaTable.put(id, newEntry);
            }
            else
            {
                deltaTable.put(id, value);
            }            
        }
        
        double[] valueSet = new double[deltaTable.size()];
        for (  Entry<Integer,Double> entry : deltaTable.entrySet() )
        {
            valueSet[entry.getKey()] = entry.getValue();
        }
        
        Arrays.sort(valueSet);
        Vector<Integer> matches = new Vector<Integer>();
        for ( double d : valueSet )
        {
            matches.add(deltaTable.inverse().get(d));
            if( matches.size() == 5 )
                break;
        }
        
        return matches;
    }

    /**
     * @return the imageMatrix
     */
    public RealMatrix getImageMatrix()
    {
        return imageMatrix;
    }

    /**
     * @param imageMatrix the imageMatrix to set
     */
    public void setImageMatrix( RealMatrix imageMatrix )
    {
        this.imageMatrix = imageMatrix;
    }
    
    /**
     * @return the transformedMatrix
     */
    public RealMatrix getTransformedMatrix()
    {
        return transformedMatrix;
    }
    
    /**
     * @param transformedMatrix the transformedMatrix to set
     */
    public void setTransformedMatrix( RealMatrix transformedMatrix )
    {
        this.transformedMatrix = transformedMatrix;
    }
    
    /**
     * @return the iterationCount
     */
    public int getIterationCount()
    {
        return iterationCount;
    }
    
    /**
     * @param iterationCount the iterationCount to set
     */
    public void setIterationCount( int iterationCount )
    {
        this.iterationCount = iterationCount;
    }
}
